// Copyright 2011 Google Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

/**
 * @fileoverview The ChromeExtension object contains the data structures and
 * functionality necessary to represent a Chrome Extension.  The object creates
 * sandboxes for the background and content scripts that isolate the
 * extension's code from other Javascript running on the page and at the
 * browser level.  Each piece is tied together by creating an object that
 * mimics the Chrome Extension API.  The object is created by associating the
 * API names to Firefox functionality that mimics it.
 *
 * When the object is created a the manifest.json for the user's extension
 * is retrieved via XMLHttpRequest and parsed.  The appropriate data is
 * loaded and structures are created.  The primary structures include those
 * for the background and the various content scripts.
 *
 * @author jasonstredwick@google.com (Jason Stredwick)
 */


goog.provide('mimic.ChromeExtension');

goog.require('console');
goog.require('mimic.BrowserAction');
goog.require('mimic.Extension');
goog.require('mimic.TabManager');
goog.require('mimic.Tabs');
goog.require('mimic.WindowManager');
goog.require('mimic.manifest');
goog.require('mimic.utils');



/**
 * Create an object to encapsulate a Chrome Extension that is wrapped by the
 * Mimic extension.
 * @param {string} alias The alias of the owning extension.
 * @constructor
 */
mimic.ChromeExtension = function(alias) {
  console.log('Creating extension - ' + alias);

  // State information
  /**
   * A flag to determine whether or not the extension is enabled.
   * @type {boolean}
   * @private
   */
  this.enabled_ = false;

  /**
   * The alias for the ChromeExtension.
   * @type {string}
   * @private
   */
  this.alias_ = alias;

  /**
   * A map of browser level signal handlers by id.
   * @type {!Object}
   * @private
   */
  this.handlers_ = {
    close: goog.bind(this.handleWindowClose_, this),
    loaded: goog.bind(this.handleWindowLoaded_, this),
    update: goog.bind(this.handleUpdateContent_, this)
  };

  // Extension specific data
  /**
   * Create an object to hold background units consisting of a sandbox and set
   * of signals mapped by id.
   * @type {!Object}
   * @private
   */
  this.backgroundUnitMap_ = {};

  /**
   * Create an object to hold content script units consisting of a sandbox and
   * set of signals mapped by tab id.
   * @type {!Object}
   * @private
   */
  this.contentUnitMap_ = {};

  // Initialize extension.
  var data = mimic.manifest.processManifest(alias);
  if (!data) {
    throw 'ERROR (mimic.ChromeExtension): Failed to create a new Chrome ' +
          'extension because manifest processing failed.';
  }

  /**
   * Contains a Chrome Extension's manifest data as an object.
   * @type {!Object}
   * @private
   */
  this.manifestData_ = data || {};

  mimic.BrowserAction.init(this.alias_,
                           this.manifestData_.rootUrl,
                           this.manifestData_.browserAction);

  this.enable();
};


/**
 * "Destroys" the Manager instance by "destroying" its data.
 */
mimic.ChromeExtension.prototype.destroy = function() {
  var alias = this.alias_;

  // Clean up UI.
  this.disable();

  mimic.BrowserAction.destroyWindows(alias);

  // Clean up internal data.
  this.alias_ = '';

  this.contentSandboxes_ = {};
  this.backgroundSandboxes_ = {};
  this.manifestData_ = {};

  console.log('Extension destroyed - ' + alias);
};


/**
 * Disables the extension.
 */
mimic.ChromeExtension.prototype.disable = function() {
  this.enabled_ = false;

  // Clear up browser
  mimic.BrowserAction.hide(this.alias_, this.manifestData_.browserAction);

  //  Unhook browser level signal listeners.
  var wm = mimic.WindowManager.getInstance();
  wm.onClose.removeListener(this.handlers_.close);
  wm.onLoad.removeListener(this.handlers_.loaded);
  var tm = mimic.TabManager.getInstance();
  tm.onUpdate.removeListener(this.handlers_.update);

  // Destroy units
  for (var key in this.contentUnitMap_) {
    this.destroyUnit_(this.contentUnitMap_[key]);
    delete this.contentUnitMap_[key];
  }
  for (key in this.backgroundUnitMap_) {
    this.destroyUnit_(this.backgroundUnitMap_[key]);
    delete this.backgroundUnitMap_[key];
  }

  this.contentUnitMap_ = {};
  this.backgroundUnitMap_ = {};
};


/**
 * Enables the extension.
 */
mimic.ChromeExtension.prototype.enable = function() {
  //  Hook browser level signal listeners.
  var wm = mimic.WindowManager.getInstance();
  wm.onClose.addListener(this.handlers_.close);
  wm.onLoad.addListener(this.handlers_.loaded);
  var tm = mimic.TabManager.getInstance();
  tm.onUpdate.addListener(this.handlers_.update);

  // Hookup browser
  mimic.BrowserAction.show(this.alias_, this.manifestData_.browserAction);

  // Create background object for the main background page.
  // Prepare extra background script features.
  var Cc = Components.classes;
  var Ci = Components.interfaces;
  var component = '@mozilla.org/xmlextras/xmlhttprequest;1';

  var items = {
    // Note: This function poses as the constructor in that calling it with
    // new will return an instance.  However, just calling the function also
    // returns an instance.
    // TODO (jasonstredwick): Figure out when requesting a Firefox chrome url
    // results in a syntax error.
    // TODO (jasonstredwick): Figure out if this approach allows one to access
    // Firefox chrome urls for other extensions.  Then find out if the answer
    // is the expected result; i.e. how does it work in Chrome Extension's
    // background scripts.
    'XMLHttpRequest': function() {
      return Cc[component].createInstance(Ci.nsIXMLHttpRequest);
    }
  };

  var data = this.manifestData_;

  // Determine the principal for the main background script.
  var windows = mimic.WindowManager.getInstance().getAllChromeWindows();
  var win = windows.length > 0 ? windows[0] : null;
  var principal = win ? win.document.nodePrincipal : data.backgroundPage;

  if (this.manifestData_.backgroundScript) {
      this.createUnit_('main',                // id
                       principal,             // principal/origin
                       {},                    // window object
                       data.backgroundScript, // script
                       items,                 // items to add to sandbox
                       true);                 // Is background script
  }

  this.enabled_ = true;
};


/**
 * Returns whether or not the extension is enabled.
 * @return {boolean} The enable flag.
 */
mimic.ChromeExtension.prototype.isEnabled = function() {
  return this.enabled_;
};


/**
 * Create the Chrome Extension api for background scripts to be used within a
 * sandbox.
 * @param {string|number} id The id for the code unit.
 * @param {!Object} unit The unit corresponding to the owner of the API object.
 * @return {!Object} The chrome api object.
 * @private
 */
mimic.ChromeExtension.prototype.createBackgroundAPI_ = function(id, unit) {
  var csUnits = this.contentUnitMap_;
  var sandbox = unit.sandbox;
  var signals = unit.signals;

  return {
    'browserAction': {
      'onClicked':
          mimic.utils.addSignal(signals.browserAction.onClicked),
      'setBadgeBackgroundColor':
          goog.partial(mimic.BrowserAction.setBadgeBackgroundColor,
                       this.alias_,
                       this.manifestData_.browserAction),
      'setBadgeText': goog.partial(mimic.BrowserAction.setBadgeText,
                                   this.alias_,
                                   this.manifestData_.browserAction),
      'setTitle': goog.partial(mimic.BrowserAction.setTitle,
                               this.alias_,
                               this.manifestData_.browserAction),
      'setIcon': goog.partial(mimic.BrowserAction.setIcon,
                              this.alias_,
                              this.manifestData_.browserAction,
                              this.manifestData_.rootUrl)
    },

    'extension': {
      'connect': function() { return {}; },
      'getURL': goog.partial(mimic.Extension.getURL,
                             this.manifestData_.rootUrl),
      'lastError': '',
      'onConnect': mimic.utils.addSignal(signals.extension.onConnect),
      'onRequest': mimic.utils.addSignal(signals.extension.onRequest),
      'sendRequest': goog.bind(signals.extension.sendRequest,
                               signals.extension,
                               this.alias_, sandbox, csUnits, null)
    },

    'tabs': {
      'sendRequest': goog.bind(signals.tabs.sendRequest, signals.tabs,
                               this.alias_, sandbox, csUnits)
    },

    'windows': {
      'create': goog.bind(mimic.WindowManager.getInstance().create,
                          mimic.WindowManager.getInstance())
    }
  };
};


/**
 * Create the Chrome Extension api for content scripts to be used within a
 * sandbox.
 * @param {string|number} id The id for the code unit.
 * @param {!Object} unit The unit corresponding to the owner of the API object.
 * @return {!Object} The chrome api object.
 * @private
 */
mimic.ChromeExtension.prototype.createContentAPI_ = function(id, unit) {
  var bgUnits = this.backgroundUnitMap_;
  var sandbox = unit.sandbox;
  var signals = unit.signals;

  return {
    'extension': {
      'lastError': '',
      'connect': goog.bind(signals.extension.connect, signals.extension,
                           this.alias_, sandbox, bgUnits, id),
      'getURL': goog.partial(mimic.Extension.getURL,
                             this.manifestData_.rootUrl),
      'onConnect': mimic.utils.addFakeSignal(),
      'onRequest': mimic.utils.addSignal(signals.extension.onRequest),
      'sendRequest': goog.bind(signals.extension.sendRequest,
                               signals.extension,
                               this.alias_, sandbox, bgUnits, id)
    }
  };
};


/**
 * Create the signals for a given unit of code.
 * @param {boolean} forBackground Whether or not the sandbox is for
 *     background code.
 * @return {!Object} A object containing signals appropriate for the type of
 *     unit; background vs content script.
 * @private
 */
mimic.ChromeExtension.prototype.createSignals_ = function(forBackground) {
  if (forBackground) {
    return {
      browserAction: new mimic.BrowserAction(),
      extension: new mimic.Extension(),
      tabs: new mimic.Tabs()
    };
  } else {
    return {
      extension: new mimic.Extension()
    };
  }
};


/**
 * Create an object to hold data specific to a unit of code within the
 * extension.  The data will contain a sandbox and set of signals.  The sandbox
 * isolates the units of code within the extension and those outside.
 * @param {string|number} id The id of the sandbox.
 * @param {Object|string} principal The url or window object from which to
 *     base permissions and origin for the new sandbox.
 * @param {!Object} winObj The window object to be used as the sandbox's
 *     window object.
 * @param {string} script The script to execute in the sandbox.
 * @param {!Object} sandboxItems An object containing items to add to the
 *     sandbox after it is created.
 * @param {boolean} forBackground Whether or not the sandbox is for
 *     background code.
 * @private
 */
mimic.ChromeExtension.prototype.createUnit_ =
    function(id, principal, winObj, script, sandboxItems, forBackground) {
  var units = forBackground ? this.backgroundUnitMap_ : this.contentUnitMap_;
  if (id in units) {
    // Don't delete the key as it is assigned a new object below.
    this.destroyUnit_(units[id]);
  }

  // Create data and associate it with the given id.
  var sandbox = new Components.utils.Sandbox(principal);
  var signals = this.createSignals_(forBackground);
  var unit = {sandbox: sandbox, signals: signals};
  units[id] = unit;

  /**
   * TODO (jasonstredwick): Return to this issue.  Currently I am
   * removing the wrapping around the window object.  I was unable
   * to successfully to set an XPCNativeWrapper to the __proto__ as
   * it can access all the normal properties except document for
   * some reason.
   */
  sandbox.__proto__ = winObj.wrappedJSObject ? winObj.wrappedJSObject : winObj;
  sandbox['console'] = console;
  sandbox['chrome'] = forBackground ?
      this.createBackgroundAPI_(id, unit) :
      this.createContentAPI_(id, unit);
  for (var key in sandboxItems) {
    sandbox[key] = sandboxItems[key];
  }

  try {
    Components.utils.evalInSandbox(script, sandbox);
  } catch (error) {
    var msg = 'Injected script failed - ' + error;
    // TODO (jasonstredwick): While each unit has its own signals and data then
    // each one needs to have its lastError updated.
    sandbox['chrome']['extension']['lastError'] = msg;
    console.error(msg);
    return;
  }
};


/**
 * Create the signals for a given unit of code.
 * @param {!Object} signals The object containing signals.
 * @private
 */
mimic.ChromeExtension.prototype.destroySignals_ = function(signals) {
  signals.BrowserAction && signals.BrowserAction.destroy();
  signals.extension && signals.extension.destroy();
  signals.tabs && signals.tabs.destroy();
};


/**
 * Destroys the give unit of data.
 * @param {!Object} unit The object containing unit data.
 * @private
 */
mimic.ChromeExtension.prototype.destroyUnit_ = function(unit) {
  // TODO (jasonstredwick): Figure out how to clear or delete a sandbox.
  delete unit.sandbox;

  this.destroySignals_(unit.signals);
  delete unit.signals;
};


/**
 * Removes actions for all enabled extensions to the closing window.
 * @param {{window: ChromeWindow}} data The data containing the ChromeWindow
 *     that is closing.
 * @private
 */
mimic.ChromeExtension.prototype.handleWindowClose_ = function(data) {
  var doc = data.window.document;
  mimic.BrowserAction.destroy(this.alias_, doc);
};


/**
 * Add actions from all enabled extensions to the newly opened window.
 * @param {{window: ChromeWindow}} data The data containing the ChromeWindow
 *     that is closing.
 * @private
 */
mimic.ChromeExtension.prototype.handleWindowLoaded_ = function(data) {
  var doc = data.window.document;
  mimic.BrowserAction.updateWindow(this.alias_,
                                   this.manifestData_.browserAction,
                                   doc);
};


/**
 * Handles tab updates.
 * @param {{location: string, id: number, window: !Object}} data The data sent
 *     when a tab updates.
 * @private
 */
mimic.ChromeExtension.prototype.handleUpdateContent_ = function(data) {
  var id = data.id;
  var url = data.location;
  var win = data.window;

  // Destroy all content scripts associated with the given tab id.
  var unit = this.contentUnitMap_[id];
  if (unit) {
    this.destroyUnit_(unit);
    delete this.contentUnitMap_[id];
  }

  // Loop over content scripts
  var contentScripts = this.manifestData_.contentScripts;
  for (var i = 0; i < contentScripts.length; ++i) {
    var script = contentScripts[i];
    var matches = script.matches;

    for (var j = 0; j < matches.length; ++j) {
      // if match then "inject" content script
      if (matches[j].test(url)) {
        if (script.css) {
          var css = script.css;

          // Not sure how to inject CSS, so will use the literal way by
          // adding a style tag to the document.  There are alternative ways
          // such as using nsIStyleSheetService but it adds to all documents
          // in all windows immediately.  It does not allow for targeted
          // injection.
          var element = win.document.createElement('style');
          var heads = win.document.getElementsByTagName('head');
          if (element && heads.length > 0) {
            element.type = 'text/css';
            element.innerHTML = css;
            heads[0].appendChild(element);
          }
        }

        if (script.js) {
          var js = script.js;
          this.createUnit_(id,
                           win,    // principal/origin
                           win,    // window object
                           js,     // script
                           {},     // items to add to sandbox
                           false); // Is background script
        }

        break;
      }
    }
  }
};

